'''Simple request-response TCP server.

Shared between master monitoring server, and individual monitord nodes.
Nodes only respond to pings - master responds to other commands too.
'''
import struct
import threading
import socket
import net

BUFFSIZE=4096 #recv buffer

# TODO: parallelize via coroutine
def connection_handler(sock, addr, request_handler):
    '''Individual connection handler, non-blocking.

Request-response protocol definition:
    Request packet structure: [WORD length of packet][BYTE type id][VOID data]
    Response packet structure: [WORD length of packet][VOID data]
To avoid redundancy, length field is not passed to request handler.

Communication goes request - response - request - response - ...etc
The peers must fully receive a response before they can formulate a request!

This has a neat implication: you don't need to care about receiving while
sending and vice versa. Not a huge deal, but results in non-blocking code that
looks almost exactly like blocking code :)

Speaking of non-blocking: this function is a coroutine. It has multiple entry
points. The obvious one, and two 'yield' statements which relinquish control
to the caller, who can then resume execution on the yield statement later. (And
even pass things into the yield statement like it's a function call!)

These things are everywhere in Python: mostly used to implement iterators.
(Think about the standard 'for n in iter: operate_on(n)' syntax.) They also get
used to elegantly implement asynchronous tasks. (There are several large
frameworks that focus on this idea, Twisted being probably the biggest one.)

The current implementation assumes the request->response handler is instant.
This is -mostly- the case.
    '''
    print 'Client connected:',addr
    sock.settimeout(net.timeout)
    acc = '' #data accumulator
    try:
        while 1: 
            # receive and accumulate data
            # vvv INSERT YIELD 'CALLBACK' HERE! vvv
            chunk = sock.recv(BUFFSIZE)
            if not chunk:
                # check for 0-bytes-received disconnect
                print 'Client disconnected:',addr
                break
            acc += chunk

            # check if an entire packet has been accumulated
            if len(acc)>2:
                size = struct.unpack('!H',acc[:2])[0]
                # pop it off and handle it, if so
                if len(acc)>=size:
                    request,acc = acc[:size],acc[size:]
                    print 'Client',addr,'sent',size,'byte request.'
                    # make sure not to pass packet length into handler
                    response = request_handler(request[2:], addr)
                    response = struct.pack('!H',len(response)+2)+response
                    print 'Sending',len(response),'byte response to',addr
                    # ensure entire response is sent
                    # vvv INSERT YIELD 'CALLBACK' HERE! vvv
                    sent = sock.send(response)
                    while sent<len(response):
                        response=response[sent:]
                        sent = sock.send(response)
                        # I forget, does 0 bytes sent mean conn closed too?
                        # Guess it's not -that- bad if it is. We can afford
                        # a few bugs.
    except socket.timeout:
        print 'Client timed out:',addr
    finally:
        #print 'Cleaning up client:',addr
        sock.shutdown(socket.SHUT_RDWR)
        sock.close()
        
def loop(request_handler):
    'Infinite TCP dispatcher server loop.'
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        server.bind((socket.gethostname(), net.port))
        server.listen(5)
        while 1:
            client,addr = server.accept()
            th = threading.Thread(target=connection_handler, 
                             args=(client,addr,request_handler))
            th.start()
    finally:
        #server.shutdown(socket.SHUT_RDWR) # needs to be closed one way, but 
        server.close()                      # which??

if __name__=='__main__':
    import protocol.node
    loop(protocol.node.handler)

